/* OpenStereo Library - A library to support the development of real time stereoscopic applications.
 * Copyright (C) 2007  Pedro Leite.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>

#include <SL/sl_plugin.h>

#include <Cg/cg.h>
#include <Cg/cgGL.h>

#ifdef WIN32
	#include <windows.h>

	BOOL APIENTRY DllMain (HINSTANCE hInst, DWORD reason, LPVOID reserved) {
		switch (reason) {
			case DLL_PROCESS_ATTACH:
			case DLL_PROCESS_DETACH:
			case DLL_THREAD_ATTACH:
			case DLL_THREAD_DETACH:
				break;
		}
		return TRUE;
	}
#endif

#include <GL/glu.h>


static char* pluginName = "SLoGL_NoRTT - The Stereo Library over Graphics Library Plugin without RenderToTexture Support.";
static char* pluginDescription = "The SLoGL plugin implements a plugins for the SL, by utilizing the OpenGL functions.";

static char* programSource = "half3 main ( half2 texCoord : TEXCOORD0, uniform half3x3 matrixL, uniform half3x3 matrixR, uniform sampler2D textureL, uniform sampler2D textureR ) : COLOR { return mul(matrixL, tex2D(textureL, texCoord)) + mul(matrixR, tex2D(textureR, texCoord)); }";

// Cg variables
static CGcontext context = 0;

static float matrices[10][9] = {
	{0.299f, 0.587f, 0.114f, 0, 0, 0, 0, 0, 0}, // SL_TRUE_COLOR_ANAGLYPH left
	{0, 0, 0, 0, 0, 0, 0.299f, 0.587f, 0.114f}, // SL_TRUE_COLOR_ANAGLYPH right
	{0.299f, 0.587f, 0.114f, 0, 0, 0, 0, 0, 0}, // SL_GRAY_ANAGLYPH left
	{0, 0, 0, 0.299f, 0.587f, 0.114f, 0.299f, 0.587f, 0.114f}, // SL_GRAY_ANAGLYPH right
	{0.299f, 0.587f, 0.114f, 0, 0, 0, 0, 0, 0}, // SL_HALF_COLOR_ANAGLYPH left
	{0, 0, 0, 0, 1, 0, 0, 0, 1}, // SL_HALF_COLOR_ANAGLYPH right
	{1, 0, 0, 0, 0, 0, 0, 0, 0}, // SL_COLOR_ANAGLYPH left
	{0, 0, 0, 0, 1, 0, 0, 0, 1}, // SL_COLOR_ANAGLYPH right
	{0, 0.7f, 0.3f, 0, 0, 0, 0, 0, 0}, // SL_OPTIMIZED_ANAGLYPH left
	{0, 0, 0, 0, 1, 0, 0, 0, 1} // SL_OPTIMIZED_ANAGLYPH right
};

typedef struct StereoData {
	CGparameter matrix;
	CGparameter texture;
	SLuint textId;
} StereoData;

static void cgErrorCallback(void) {
	const char* errorString = cgGetErrorString(cgGetError());
	fprintf(stderr, "%s\n", errorString);
}

SL_PLUGIN_API SLbool SL_PLUGIN_API_ENTRY loadPlugin(void) {
	context = cgCreateContext();
	cgSetErrorCallback(cgErrorCallback);

	return SL_TRUE;
}

SL_PLUGIN_API const char* SL_PLUGIN_API_ENTRY getPluginName(void) {
	return pluginName;
}

SL_PLUGIN_API const char* SL_PLUGIN_API_ENTRY getPluginDescription(void) {
	return pluginDescription;
}

static GLuint allocateTexture(GLuint size) {
	GLuint textureID;
	glGenTextures(1, &textureID);

	glBindTexture(GL_TEXTURE_2D, textureID);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size, size, 0, GL_BGRA_EXT,
		GL_UNSIGNED_BYTE, 0);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	return textureID;
}

SL_PLUGIN_API SLStereoData SL_PLUGIN_API_ENTRY allocateStereoData(SLuint width, SLuint height) {
	SLuint max = width >= height ? width : height;
	SLuint nextPowerOfTwo = 1;
	StereoData* left = 0;
	StereoData* right = 0;
	SLStereoData data = 0;
	CGprogram fragmentProgram;

	while ((nextPowerOfTwo <<= 1) < max);

	fragmentProgram = cgCreateProgram(context, CG_SOURCE,
		programSource, CG_PROFILE_ARBFP1, 0, 0);
	cgGLLoadProgram(fragmentProgram);

	left = (StereoData*) malloc(sizeof(StereoData));
	left->textId = allocateTexture(nextPowerOfTwo);
	left->matrix = cgGetNamedParameter(fragmentProgram, "matrixL");
	left->texture = cgGetNamedParameter(fragmentProgram, "textureL");
	
	right = (StereoData*) malloc(sizeof(StereoData));
	right->textId = allocateTexture(nextPowerOfTwo);
	right->matrix = cgGetNamedParameter(fragmentProgram, "matrixR");
	right->texture = cgGetNamedParameter(fragmentProgram, "textureR");

	data = (SLStereoData) malloc(sizeof(struct _SLStereoData));
	data->left = left;
	data->right = right;
	data->stereo = fragmentProgram;

	return data;
}

SL_PLUGIN_API void SL_PLUGIN_API_ENTRY destroyStereoData(SLStereoData data) {
	if (data) {
		if (data->left) {
			StereoData* left = (StereoData*) data->left;
			glDeleteTextures(1, &left->textId);
			free(data->left);
			data->left = 0;
		}
		if (data->right) {
			StereoData* right = (StereoData*) data->right;
			glDeleteTextures(1, &right->textId);
			free(data->right);
			data->right = 0;
		}
		if (data->stereo) {
			cgDestroyProgram((CGprogram) data->stereo);
			data->stereo = 0;
		}
		free(data);
	}
}

static void writeTexture(GLuint width, GLuint height, GLbitfield mask) {
	SLuint max = width >= height ? width : height;
	SLuint nextPowerOfTwo = 1;
	float maxH = 0;
	float maxW = 0;

	GLboolean lighting, not_texture, blend;

	while ((nextPowerOfTwo <<= 1) < max);

	maxH = height / (float) nextPowerOfTwo;
	maxW = width / (float) nextPowerOfTwo;

	glDrawBuffer(GL_BACK);
	lighting = glIsEnabled(GL_LIGHTING);
	not_texture = !glIsEnabled(GL_TEXTURE_2D);
	blend = glIsEnabled(GL_BLEND);

	if (lighting)
		glDisable(GL_LIGHTING);

	if (blend)
		glDisable(GL_BLEND);

	glClear(mask);
	glLoadIdentity();

	if (not_texture)
		glEnable(GL_TEXTURE_2D);

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0, width, height, 0, -1, 1);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	glBegin(GL_QUADS); {
		glTexCoord2f(0, maxH);
		glVertex2f(0, 0);

		glTexCoord2f(0, 0);
		glVertex2i(0, height);

		glTexCoord2f(maxW, 0);
		glVertex2i(width, height);

		glTexCoord2f(maxW, maxH);
		glVertex2i(width, 0);
	} glEnd();

	if (not_texture)
		glDisable(GL_TEXTURE_2D);

	if (blend)
		glEnable(GL_BLEND);

	if (lighting)
		glEnable(GL_LIGHTING);

	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();
}

static void normalise(struct _SLVector3* vec) {
	float length;

	length = (float) sqrt(vec->x * vec->x + vec->y * vec->y + vec->z * vec->z);
	if (length != 0) {
		vec->x /= length;
		vec->y /= length;
		vec->z /= length;
	} else {
		vec->x = 0;
		vec->y = 0;
		vec->z = 0;
	}
}

#define DTOR 0.0174532925f

SL_PLUGIN_API void SL_PLUGIN_API_ENTRY renderStereo(SLViewport viewport, SLScene scene,
SLStereoData stereoData) {

	SLCamera camera = slGetCamera(viewport);
	SLuint width = slGetWidth(viewport);
	SLuint height = slGetHeight(viewport);
	SLAnaglyphMethod method = slGetAnaglyphMethod(viewport);
	StereoData* leftD = (StereoData*) stereoData->left;
	StereoData* rightD = (StereoData*) stereoData->right;
	CGprogram fragmentProgram = (CGprogram) stereoData->stereo;
	GLbitfield mask = GL_STENCIL_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT;

	float left, right, bottom, top, _near = 0.001f, _far = 10000;
	float ratio, radians, wd2, ndfl;
	struct _SLVector3 r, dir = camera->viewDirection, up = camera->viewUp,
		pos = camera->viewPosition;

	ratio = (float) width / (float) height;
	// aperture divided by two in radians
	radians = DTOR * camera->aperture * 0.5f;
	// width of near plane divided by 2 [through tan(teta/2) = wd2 / near]
	wd2 = _near * (float) tan(radians);
	//
	ndfl = _near / camera->focalLength;

	r.x = dir.y * up.z - dir.z * up.y;
	r.y = dir.z * up.x - dir.x * up.z;
	r.z = dir.x * up.y - dir.y * up.x;
	normalise(&r);

	r.x *= camera->eyeSeparation * 0.5f;
	r.y *= camera->eyeSeparation * 0.5f;
	r.z *= camera->eyeSeparation * 0.5f;

	glPushAttrib(GL_VIEWPORT_BIT);
	glViewport(0, 0, width, height);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	left = -ratio * wd2 - 0.5f * camera->eyeSeparation * ndfl;
	right = ratio * wd2 - 0.5f * camera->eyeSeparation * ndfl;
	top = wd2;
	bottom = -wd2;

	glFrustum(left, right, bottom, top, _near, _far);

	glMatrixMode(GL_MODELVIEW);
	glClear(mask);
	glLoadIdentity();

	gluLookAt(pos.x + r.x, pos.y + r.y, pos.z + r.z, pos.x + r.x + dir.x,
		pos.y + r.y + dir.y, pos.z + r.z + dir.z, up.x, up.y, up.z);

	scene();

	glBindTexture(GL_TEXTURE_2D, rightD->textId);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width,
		height);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	left = -ratio * wd2 + 0.5f * camera->eyeSeparation * ndfl;
	right = ratio * wd2 + 0.5f * camera->eyeSeparation * ndfl;
	top = wd2;
	bottom = -wd2;

	glFrustum(left, right, bottom, top, _near, _far);

	glMatrixMode(GL_MODELVIEW);
	glClear(mask);
	glLoadIdentity();

	gluLookAt(pos.x - r.x, pos.y - r.y, pos.z - r.z, pos.x - r.x + dir.x,
		pos.y - r.y + dir.y, pos.z - r.z + dir.z, up.x, up.y, up.z);

	scene();

	glBindTexture(GL_TEXTURE_2D, leftD->textId);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width,
		height);

	glPopAttrib();

	cgGLSetMatrixParameterfr(leftD->matrix, matrices[method << 1]);
	cgGLSetMatrixParameterfr(rightD->matrix, matrices[(method << 1) + 1]);
	cgGLSetTextureParameter(leftD->texture, leftD->textId);
	cgGLEnableTextureParameter(leftD->texture);
	cgGLSetTextureParameter(rightD->texture, rightD->textId);
	cgGLEnableTextureParameter(rightD->texture);

	cgGLEnableProfile(CG_PROFILE_ARBFP1);
	cgGLBindProgram(fragmentProgram);

	writeTexture(width, height, mask);

	cgGLDisableProfile(CG_PROFILE_ARBFP1);
}

SL_PLUGIN_API void SL_PLUGIN_API_ENTRY unloadPlugin(void) {
	cgDestroyContext(context);
}
